package com.talkyun.openx.server;


import com.talkyun.openx.error.ExceptionHandler;
import com.talkyun.openx.plugin.echo.EchoImpl;
import com.talkyun.openx.server.codec.JsonCodec;
import com.talkyun.openx.server.codec.SecureJsonCodec;
import com.talkyun.openx.server.core.*;
import com.talkyun.openx.server.impl.FastJsonInvoker;
import com.talkyun.openx.server.impl.SimpleServiceFactory;
import com.talkyun.utils.json.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;

import static com.talkyun.openx.server.core.ServiceContext.REQ_IP;
import static com.talkyun.openx.server.core.ServiceResponse.Status.ERROR;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;

public class ServletHandler extends AbstractHandler {
    private static final String ENCODING = "utf-8";
    private static final Logger logger = LoggerFactory.getLogger(ServletHandler.class);

    private static final ServiceFactory factory = SimpleServiceFactory.getInstance();
    private static final ExceptionHandler handler = new ExceptionHandler();
    private static final JsonCodec codec = new SecureJsonCodec(factory);
    private static final ServiceInvoker invoker = new FastJsonInvoker(factory);

    public ServletHandler(ServletContext sc) {
        String context = sc.getContextPath();
        logger.info("Create servlet handler: {}", isBlank(context) ? "/" : context);
        factory.add("/openx/echo", new EchoImpl());
    }

    public void handle(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // CORS first
        String method = req.getMethod();
        if ("OPTIONS".equalsIgnoreCase(method)) {
            req.getSession().getId();
            this.doCors(req, resp);
            return;
        }
        this.doCors(req, resp);

        // Servlet handle
        String uri = req.getRequestURI();
        String data = this.doRead(req);
        // ready request
        ServiceRequest sr = codec.decode(uri, data);
        sr.setSession(req.getSession().getId());

        // ready context
        String ip = getRemoteAddress(req);
        ServiceContext ctx = ServiceContext.getContext(sr.getSession());
        ctx.put(REQ_IP, ip);
        // set request to ctx
        ctx.setServiceRequest(sr);

        ServiceResponse sp = this.invoke(ctx, sr);

        String json = sp.getResult();
        if (sp.getStatus() == ServiceResponse.Status.NORMAL) {
            this.doWrite(resp, json);
        } else {
            this.doError(resp, SC_INTERNAL_SERVER_ERROR, json);
        }
    }

    private ServiceResponse invoke(ServiceContext ctx, ServiceRequest sr) {
        try {
            return invoker.invoke(ctx, sr);
        } catch (Throwable t) {
            // handle throwable, wrap to response
            if (t instanceof ServiceException) {
                t = t.getCause(); // get raw throwable
            } else if (t instanceof InterceptorException && t.getCause() != null) {
                t = t.getCause();
            }

            logger.error("Error to invoke {} !", sr, t);
            return new ServiceResponse(sr.getSession(), ERROR, handler.encode(t));
        } finally {
            ctx.clean();
        }
    }

    private void doCors(HttpServletRequest req, HttpServletResponse resp) {
        String origin = req.getHeader("Origin");
        if (null == origin || "null".equalsIgnoreCase(origin)) {
            resp.addHeader("Access-Control-Allow-Origin", "*");
        } else {
            resp.setHeader("Access-Control-Allow-Origin", origin);
        }
        resp.addHeader("Access-Control-Allow-Methods", "GET,POST");
        resp.addHeader("Access-Control-Allow-Headers", "Set-Cookie,X-Requested-With,Content-Type");
        resp.addHeader("Access-Control-Allow-Credentials", "true");
    }

    private String doRead(HttpServletRequest req) throws IOException {
        if ("GET".equalsIgnoreCase(req.getMethod())) {
            String data = req.getQueryString();
            if (!super.isBlank(data)) {
                int pos = data.indexOf("&");
                if (pos != -1) {
                    data = data.substring(0, pos);
                }
                data = URLDecoder.decode(data, "utf-8");
            }
            return data;
        }
        // doRead from post stream
        return super.read(req.getInputStream(), ENCODING);
    }

    private void doError(HttpServletResponse resp, int code, String msg) throws IOException {
        resp.setStatus(code);
        this.doWrite(resp, msg);
    }

    private void doWrite(HttpServletResponse resp, Object obj) throws IOException {
        String json = obj instanceof String ? (String) obj : JSON.toJsonString(obj);
        resp.setCharacterEncoding(ENCODING);
        resp.setContentType("application/json");
        resp.setHeader("Cache-Control", "no-cache");
        resp.getOutputStream().write(json.getBytes(ENCODING));
    }

    private String getRemoteAddress(HttpServletRequest req) {
        String ip = req.getHeader("X-Forwarded-For");
        // proxy mode (slb|nginx)
        if (ip != null && ip.trim().length() > 0) {
            int pos = ip.indexOf(",");
            if (pos != -1) {
                return ip.substring(0, pos).trim();
            } else {
                return ip.trim();
            }
        }
        // not proxy mode
        return req.getRemoteAddr();
    }
}